import _thread
import time
import ntptime
import utime


from machine import Pin, ADC
import time

import calculate
import tools

# import json

# # 选择控制LED的GPIO引脚。例如，使用NodeMCU开发板上的D4引脚(GPIO 2)
# LED_PIN = 2
 
# # 初始化GPIO引脚模式为输出
# led = Pin(LED_PIN, Pin.OUT)


class PreState():
    hour=-1
    t=-1
    h=-1
    sunny=-1
    pre_sunny=-1
    def __init__(self,hour=-1,t=-1,h=-1,sunny=-1):
        self.set(hour,t,h,sunny)

    def reset(self):
        self.hour=-1
        self.t=-1
        self.h=-1
        self.sunny=-1

    def set_sunny(self,sunny):
        #晴朗的强度, 用温度表示!
        self.sunny=sunny

    def set(self,hour,t,h,sunny=-1):
        self.hour=hour
        self.t=t
        self.h=h
        if sunny > self.sunny:
            self.set_sunny(sunny)


def mk_out_pin(pin):
    p=Pin(pin,Pin.OUT)
    p.value(0)
    return p

# a.atten 
def mk_adc(pin):
    a=ADC(pin)
    a.atten(ADC.ATTN_11DB)
    a.width(ADC.WIDTH_12BIT)
    return a

# "/sun_water_info.json"
INFO='{"error":%d,"month":%d,"height":%d,"temperature":%d,"temperature_float":%0.2f,"target_h":%d,"target_t":%d,"adc_h1":%d,"adc_h2":%d,"adc_t":%d,"rrr_h":%d,"rrr_t":%d,"feeding":"%s","heating":"%s","event":"%s","now":%d,"time":"%02d:%02d:%02d"}'

class Agent():
    AUTO_HEAT=True
    def get_state_AUTO_HEAT(self):
        return self.AUTO_HEAT
    
    def set_state_AUTO_HEAT(self,state=True):
        # 默认 AUTO_HEAT==True
        self.AUTO_HEAT=state
        # try:
        #     pass
        # except:
        #     self.AUTO_HEAT=True

    def __init__(self,led,adc_t,adc_h,ac1,ac2,heating,feeding):
        self.AUTO_HEAT=True
        # pins
        self.led1=mk_out_pin(led) #Pin(led,Pin.OUT)
        self.is_led1_on=False
        self.pin_adc_t=mk_adc(adc_t)       
        self.pin_adc_h=mk_adc(adc_h)
        self.pin_ac1=mk_out_pin(ac1)
        self.pin_ac2=mk_out_pin(ac2)

        self.pin_heating=mk_out_pin(heating)
        self.pin_feeding=mk_out_pin(feeding)

        self.t=-1
        self.target_t=-1
        self.h=-1
        self.target_h=-1

        self.adc_t=-1
        self.adc_h=(-1,-1)

        self.R_t=-1
        self.R_h=-1

        self.T_ON=False
        self.H_ON=False

        self.t72=72

        self.timestamp0=-1
        self.info=""
        self.info_timestamp=0
        self.month=-1
        self.hour=-1
        
        self.lock = _thread.allocate_lock()
        
        self.last_hour=-1
        self.temperatures_full_day=[]
        self.temperatures_yestoday=-1
        self.history_info=None
        
        self.history_size=15
        self.history_cur=0
        self.history_adc_t=[-1]*self.history_size
        self.history_adc_h=[-1]*self.history_size
        self.latest_heating_localtime=-1

        self.init_state()
    
    def led_toggle(self):
        if self.is_led1_on:
            self.led1.off()
        else:
            self.led1.on()
        self.is_led1_on=not self.is_led1_on
        

    def init_state(self):
        self.update_state(self.history_size,11)
        for i in range(15):
            time.sleep(1)
            self.update_state()

        # for i in range(10):
        #     self.hourly(1.88+i)
        # self.temperatures_yestoday=self.temperatures_full_day
        # self.temperatures_full_day=[]
        # for i in range(10):
        #     self.hourly(1.99+i)


    def update_state(self,m=1,n=11,need_do_control=False):
        self.led_toggle()
        print("[info] update state")
        # readadc
        for _ in range(m):
            adc_list_t=[]
            adc_list_h=[]
            self.read_adc_t()
            self.read_adc_h()
            utime.sleep_ms(10)
            for _ in range(n):
                adc_list_t.append(self.read_adc_t())
                adc_list_h.append(self.read_adc_h())
                utime.sleep_ms(10)
            

            self.pin_ac1.value(0)
            self.pin_ac2.value(0)
            adc_list_t=sorted(adc_list_t)
            adc_list_h=sorted(adc_list_h,key=lambda a:a[0]/(a[1]+0.00001))
            #print("[Debug]",adc_list_h)

            self.history_adc_t[self.history_cur]=adc_list_t[n//2]
            self.history_adc_h[self.history_cur]=adc_list_h[n//2]
            self.history_cur=(self.history_cur+1)%self.history_size

        self.calculate()
        if need_do_control:
            self.control()
        

    def calculate(self):
        # get median
        self.adc_t=sorted(self.history_adc_t)[self.history_size//2]
        self.adc_h=sorted(self.history_adc_h,key=lambda a:a[0]/(a[1]+0.00001))[self.history_size//2]
        
        # 计算 R_H, R_T -> H T
        R_t,t=calculate.calculate_t(self.adc_t)
        R_h,h=calculate.calculate_h(*self.adc_h)

        self.t=round(t,2)
        self.h=int(h)
        self.R_t=int(R_t)
        self.R_h=int(R_h)


    def read_adc_t(self):
        return self.pin_adc_t.read()
    
    def read_adc_h(self):
        # digitalWrite(this->pinH_io1, LOW); LOW==OFF
        # digitalWrite(this->pinH_io2, HIGH);
        def read_one():
            self.pin_ac1.value(0)
            self.pin_ac2.value(1)
            utime.sleep_ms(10)
            self.pin_adc_h.read()
            a1=self.pin_adc_h.read()
            self.pin_ac2.value(0)
            self.pin_ac1.value(1)
            utime.sleep_ms(10)
            self.pin_adc_h.read()
            a2=self.pin_adc_h.read()
            return a1,a2
        
        return read_one()
        
    def get_history(self):
        return {
            "latest_heating":self.latest_heating_localtime,
            "today"    :self.temperatures_full_day,
            "yestoday" :self.temperatures_yestoday
        }

    
    
    def get_info(self,event="/sun_water_info.json",force=False):
        # {"error":0,"height":2,"temperature":66,"target_h":-1,"target_t":-1,"adc_h1":1822,"adc_h2":2842,"adc_t":2922,
        #   "rrr_h":23307,"rrr_t":2162,"feeding":"","heating":"","event":"/sun_water_info.json","now":7947,"time":"02:12:42"}
        timestamp=time.time()

        if force or self.info=="" or timestamp-self.info_timestamp>5:
            localtime=tools.localtime()
            if self.timestamp0<0 and tools.isUpdated(localtime):
                self.timestamp0=timestamp
                self.month=tools.get_month(localtime)

            # make new info
            feeding="ON" if self.isH_ON() else ""
            heating="ON" if self.isT_ON() else ""
            t_hour=tools.get_hour(localtime) #(localtime[3]+8)%24
            t_minute=tools.get_minute(localtime) #localtime[4]
            t_sec=tools.get_sec(localtime) #localtime[5]
            uptime=timestamp-self.timestamp0
            info= INFO%(0,self.month,self.h,int(self.t),self.t,self.target_h,self.target_t, self.adc_h[0], self.adc_h[1],self.adc_t,self.R_h,self.R_t,feeding,heating,event,uptime,t_hour,t_minute,t_sec)
            if force:
                return info
            self.info=info
            self.info_timestamp=timestamp
        
        return self.info
        
    def get_state(self):
        return self.t,self.target_t,self.h,self.target_h
    
    def turnT_ON(self,localtime=None):
        # 加热时 重置 self.STATE_PRE
        try:
            self.STATE_PRE.reset()
        except:
            pass

        self.pin_heating.value(1)
        self.T_ON=True
        if localtime is not None:
            self.latest_heating_localtime=localtime
        print("[i] turnT_ON")
        # maximum recursion depth exceeded : 把 localtime()写在外出函数!!

    def turnT_OFF(self):
        self.pin_heating.value(0)
        self.T_ON=False
        print("[i] turnT_OFF")
    
    def turnH_ON(self):
        self.pin_feeding.value(1)
        self.H_ON=True
        print("[i] turnH_ON")

    def turnH_OFF(self):
        self.pin_feeding.value(0)
        self.H_ON=False
        print("[i] turnH_OFF")

    def isH_ON(self):
        return self.H_ON
    
    def isT_ON(self):
        return self.T_ON
    
    def setTargetT(self,v,timestamp=None):
        with self.lock:
            try:
                if v<0:
                    #if self.isT_ON():
                    self.target_t=-1
                    self.turnT_OFF()
                    return True
                elif self.target_t<v:
                    if self.t<v:
                        self.target_t=v
                        if timestamp is None:
                            self.turnT_ON()
                        else:
                            self.turnT_ON(list(tools.localtime()))
                        return True
            except Exception as e:
                print("[E] setTargetT error",e)
            return False
    
    def setTargetH(self,v,more=0):
        # 封到函数里, 之前  v+more>4 提前返回 导致死锁...
        #   (虽然代码里 v+more 不会>4, 但还是修改了) 
        ##
        # 同时 修复 只能上到2格的问题. (v=2,more=1) -> h<v(即h<2时) 上到2格
        ## h>=v && (v+more>h && v+more>th)时 上到v+more
        target=-1
        with self.lock:
            try:
                if v<0:
                    #if self.isH_ON():
                    self.target_h=-1
                    self.turnH_OFF()
                    return True
                
                if self.h<v:
                    if self.target_h <v:
                        target=v
                else:
                    target=v+more
                    if target<=self.target_h or target<=self.h:
                        target=-1
                if target >0 and target<=4:
                    self.target_h=target
                    self.turnH_ON()
                    return True
                ###
            except Exception as e:
                print("[E] setTargetH error",e)
            return False

    def control(self):
        t,target_t,h,target_h=self.get_state()

        def _ctrl_h():
            t,target_t,h,target_h=self.get_state()
            if h < 0 or h > 4:
                self.turnH_OFF()
                self.setTargetH(-1)
                return
            # 没水了
            if h == 0 and target_h <= 0:
                self.setTargetH(1)
                t,target_t,h,target_h=self.get_state()
            # 超过温度自动上水.
            if h < 4 and h >= target_h:
                if t > 72:
                    # 电加热 失败时 也能上水
                    self.setTargetH(h + 1)
                    t,target_t,h,target_h=self.get_state()
                elif t > self.t72:
                    if target_t>5 and h>=1:
                        # 电加热 不上水!
                        pass
                    else:
                        # 这个 是针对 白天太阳能.  或者温度实在太高!
                        if (self.hour > 5 and self.hour < 17) or t>76:
                            self.setTargetH(h + 1)
                            t,target_t,h,target_h=self.get_state()
            # 上水
            if target_h > 0 and target_h <= 4 and target_h > h:
                self.turnH_ON()

        def _ctrl_t():
            t,target_t,h,target_h=self.get_state()
            if t < 0 or t > 125:
                self.turnT_OFF()
                self.setTargetT(-1)
                return
            # Heating
            if target_t > 35 and target_t <= 70 and target_t > t:
                self.turnT_ON()

                
        _ctrl_h()
        _ctrl_t()

        # OFF
        t,target_t,h,target_h=self.get_state()
        if target_t <= 0 or target_t<=t or t >= 80:
            self.turnT_OFF()
            self.setTargetT(-1)
            t,target_t,h,target_h=self.get_state()
        if target_h <= 0 or target_h<=h or h>=4:
            self.turnH_OFF()
            self.setTargetH(-1)
            t,target_t,h,target_h=self.get_state()


    def hourly(self,hour,minute=0,is_first=True):
        if is_first and (hour<=0 or hour>23):
            hour=0
            self.temperatures_yestoday=self.temperatures_full_day
            self.temperatures_full_day=[]
        state=[hour,minute]
        state.extend(self.get_state())
        self.temperatures_full_day.append(state)
        # self.history_info=None
    
        
    # def get_season(self,month):
    #     if 3 <= month <= 5:
    #         return 0
    #     elif 6 <= month <= 8:
    #         return 1
    #     elif 9 <= month <= 11:
    #         return 2
    #     elif month in [12,1,2]:
    #         return 3
    #     else:
    #         return None
    
    # 记录 热度
    STATE_PRE=PreState()

    def feed_water_by_month_and_hour(self,month,hour):
        if hour<=0 or hour>=24:
            return

        t,target_t,h,target_h=self.get_state()
        if target_t>10:
            return
        if h>=4 or h<=0: # h==0 会自动上水.
            return
        
        def _t_to_h(t1,t2,t3):
            if h==1:
                if t>t1:
                    return 2
            elif h==2:
                if t>t2:
                    return 3
            elif h==3:
                if t>t3:
                    return 4
            return -1

        
        def enable(h_dst):
            if h_dst>=0 and h_dst<=4 and h_dst>h and h_dst>target_h:
                self.setTargetH(h_dst)
            
        def calculateTargetByState(heat_target):
            def _cal():
                dst_h=-1
                if hour<7 or target_t > t:
                    # 重置状态: 每天 || 电加热中.
                    self.STATE_PRE.reset()
                    return dst_h
                if hour> 14:
                    return dst_h
                if t>72:
                    # 这个温度 不需要这里 计算.
                    return dst_h

                if h < 2 or h==4 or target_h==4:
                    # 每天 7点~12点自动上水到 2
                    # 在 16点 检查:  保持至少2格
                    return dst_h

                # if target_h!=h and target_h>0:
                #     # 在 上水中
                #     return dst_h

                # 写入状态

                if self.STATE_PRE.hour<0:
                    if h>=target_h:
                        # 水位静止 && t>10(即温度正常) 时 写入
                        if t>10:
                            # 热容量 简化表示: t*h, 这个不准, 要体积才行!
                            # self.STATE_PRE=PreState(hour,t*h)
                            self.STATE_PRE.set(hour,t,h)
                            # self.STATE_PRE=PreState(hour,t,h) 
                    return dst_h

                if t<25:
                    # 超过这个温度 再计算.
                    return dst_h

                # t:float
                deltaT=t-self.STATE_PRE.t # 如何处理 波动 异常? 
                # deltaH=h-self.STATE_PRE.v2

                
                # 如果 原本 温度就很高?! 导致无法吸热! delta
                ## 方案1,2,3
                if hour<12 and t>heat_target+1:
                    # 方案1, +1格试试, -> 3||4
                    dst_h=max(dst_h,h+1)
                # 方案2, 外部获取天气气温来 控制.
                # 方案3, 不控制, 自然降温||手动上水.

                # 产生了 温度的上升!
                if t>50:
                    if deltaT<0.5:
                        return dst_h
                elif t>40:
                    if deltaT<1:
                        return dst_h
                elif t>30:
                    if deltaT<1.5:
                        return dst_h
                else:
                    if deltaT<2.2:
                        return dst_h

                
                if t>self.STATE_PRE.sunny:
                    sunny=t
                    self.STATE_PRE.set_sunny(sunny)
                else:
                    return dst_h

                # t==sunny
                # heat_target-t <= 5 不会开启加热
                # 1,2,12==55; 3,4==50; 5,11==48
                if sunny > heat_target+2:
                    dst_h=4
                elif sunny > heat_target-2:
                    if hour<=12:
                        dst_h=4
                    elif hour>12 and hour <17:
                        dst_h=max(dst_h,h+1)
                elif sunny > 50:
                    if hour <= 13:
                        dst_h=max(dst_h,h+1)
                    elif hour <= 15:
                        dst_h=max(dst_h,min(3,h+1))
                elif sunny > 33:
                    if hour<=11: # or (sunny>40 and hour<=13):
                        dst_h=max(dst_h,3)
                
                if dst_h>2: # 3,4
                    if dst_h>4:
                        return dst_h
                    if target_h>=dst_h:
                        return dst_h
                    if h>=dst_h:
                        return dst_h
                    # 每次 计算完 h产生增加 , 重置.
                    self.STATE_PRE.reset()
                    # # 保持当天的 sunny值, 晴朗度
                    # self.STATE_PRE.pre_sunny=sunny
                    return dst_h
            try:
                result=_cal()
                if result is None:
                    return -1
                return result
            except:
                return -1
            

        h_dst=-1
        h_dst2=-1
        if h==1 and hour >11 and hour <16:
            h_dst=2
        if hour<7:
            self.STATE_PRE.reset()

        if month>=6 and month<=10:
            # 这些月份(夏)不自动电加热 (>50℃自动上水)
            if hour <7: # in [1,2,3,4,5,6]:
                h_dst=_t_to_h(25,40,45)
            # elif hour <12: # in [7,8,9,10,11,12]:
            #     # h_dst=_t_to_h(10,40,45)
            elif hour<16: # [8,9,10,...15]
                # 直接 4格
                h_dst=4
            elif hour >=18: # [18,19,20,21,22]:
                h_dst=_t_to_h(49,46,45)
        elif month in [5,11]:
            # month in [5,11]=48℃ (>61℃自动上水)
            if hour <=3:
                pass
            elif hour <7: # in [4,5,6]:
                h_dst=_t_to_h(25,55,55)
            elif hour <12: # in [7,8,9,10,11,12]:
                h_dst=_t_to_h(10,55,55)
            elif hour >=18: # [18~23]:
                h_dst=_t_to_h(56,66,66) # 65为加热温度!!
            h_dst2=calculateTargetByState(48)
        elif month in [3,4]:
            # month in [3,4]=50℃ (>65℃自动上水)
            if hour <=3:
                pass
            elif hour <7: # in [4,5,6]:
                h_dst=_t_to_h(25,63,63)
            elif hour <12: # in [7,8,9,10,11,12]:
                h_dst=_t_to_h(10,63,63)
            elif hour >=18: # [18~23]:
                h_dst=_t_to_h(63,66,66)
            h_dst2=calculateTargetByState(50)
        else:
            # month in [1 2 12] =55℃ (>65℃自动上水)
            if hour <=3:
                pass
            elif hour <7: # in [4,5,6]:
                h_dst=_t_to_h(35,63,63)
            elif hour <12: # in [7,8,9,10,11,12]:
                h_dst=_t_to_h(10,63,63)
            elif hour >=18: # [18,19,20,21,22]:
                h_dst=_t_to_h(63,66,66)
            h_dst2=calculateTargetByState(55)
        try:
            h_dst=max(h_dst,h_dst2)
        except:
            pass
        enable(h_dst)

    ## todo : 用一年中的第几天 最好,而不是月份
    ## 赤道: 春分~3/20 秋分~9/22
    # 冬季 1 2 12 
    # 春: 3 4
    # 春末: 5
    # summer: 6 7 8 
    # 秋天: 9 10 11

    def _daily(self):
        isTimeSet=False
        def _setTargetT_withMonth(month,temp,is_second=False):
            # temp=55
            if is_second:
                # 当天 第二次 加热
                temp=temp-5
            if month>=6 and month<=10:
                # 夏天(6 7 8) 秋天(9 10) 不自动加热.
                self.t72=50
                return

            if month in [0,1,2,12]:#冬季
                self.t72=65
                # temp=temp # temp=temp+5 # 需要更高温度时 手动加热
            elif month in [3,4]:
                self.t72=65
                temp=temp-5 # 55-5=50 # 50-5=45
            elif month in [5,11]:
                self.t72=61
                temp=temp-7 # 48 # 48-5=43
            else:
                return



            t,target_t,h,target_h=self.get_state()
            if t+5>temp:
                return
            if self.AUTO_HEAT:
                # 自动加热!
                self.setTargetT(temp)

        flag_done_for_auto_feeding_h2 = False
        flag_done_for_keep_h2_before_4pm = False
        flag_done_for_auto_heating1 = False
        flag_done_for_auto_heating2 = False
        
        # 20min 一次
        while True:
            # 10分钟
            time.sleep(600)
            isTimeSet=True
            localtime=tools.localtime()
            if not tools.isUpdated(localtime):
                ntptime.settime()
                time.sleep(15)
                localtime=tools.localtime()
                if not tools.isUpdated(localtime):
                    isTimeSet=False
                
        
            hour = tools.get_hour(localtime) #(localtime[3]+8)%24
            month= tools.get_month(localtime) #localtime[1]
            minute= tools.get_minute(localtime) #
            print("[i] daily @",hour,minute)
            if isTimeSet:
                self.month=month
                self.hour=hour

                self.feed_water_by_month_and_hour(month,hour)

            if hour == 0:
                flag_done_for_auto_feeding_h2 = False
                flag_done_for_keep_h2_before_4pm = False
                flag_done_for_auto_heating1 = False
                flag_done_for_auto_heating2 = False


            # if hour >= 7 and hour <= 16:
            #     self.setTargetH(2)
            if not flag_done_for_auto_feeding_h2 and hour >= 7 and hour <= 12:
                #// _setTargetH(3)
                #//      手动上水 == 3格
                #//  设置为2 . 目的. 省电 . 当不够用时. 再调或者 手动上水.
                #//    (超过70多度时 , 也能自动上水+1格)
                self.setTargetH(2)
                if hour == 12:
                    flag_done_for_auto_feeding_h2 = True


            # 4pm :  height>=2
            if not flag_done_for_keep_h2_before_4pm and hour == 16:
                self.setTargetH(2)
                flag_done_for_keep_h2_before_4pm = True # 不要用判断是否执行过, 16pm >=2格水

            # 时钟 更新过, 自动加热生效!!
            if isTimeSet:
                if not flag_done_for_auto_heating1 and hour == 16:
                    _setTargetT_withMonth(month,55)
                    flag_done_for_auto_heating1 = True
                if not flag_done_for_auto_heating2 and hour == 17:
                    _setTargetT_withMonth(month,55,flag_done_for_auto_heating1)
                    flag_done_for_auto_heating2 = True

            if hour != self.last_hour:
                self.last_hour=hour
                self.hourly(hour,minute,is_first=True)
            # else:
            #     self.hourly(hour,minute,is_first=False)
            
            # 10分钟
            time.sleep(600)
        
    def job_daily(self):
        while True:
            try:
                self._daily()
            except Exception as e:
                print("[E] daily job error",e)
                time.sleep(30)

    def job_update(self):
        for _ in range(5):
            try:
                # 启动初期  水位adc不准!!
                time.sleep(10)
                self.update_state()
            except Exception as e:
                print("[E] job_update error 0",e)
            

        while True:
            try:
                time.sleep(6)
                self.update_state()
            except Exception as e:
                print("[E] job_update error",e)
            
            try:
                time.sleep(4)
                self.update_state(need_do_control=True)
            except Exception as e:
                print("[E] job_update error 2",e)



# 12 13
PIN_LED1=12

# temperature
PIN_ADC_T = 3
# Hight
PIN_ADC_H = 2

PIN_AC1 = 6
PIN_AC2 = 7


# relay FOR heating (GPIO 11  false=2.8v??? 换成gpio 4|5)
PIN_HEATING = 5

# mosfet , 12v for water feeding.
PIN_WATERFEED = 10

_agent_lock = _thread.allocate_lock()
_agent=None
def init_agent(led=PIN_LED1,adc_t=PIN_ADC_T,adc_h=PIN_ADC_H,ac1=PIN_AC1,ac2=PIN_AC2,heating=PIN_HEATING,feeding=PIN_WATERFEED):
    global _agent
    try:
        _agent=Agent(led,adc_t,adc_h,ac1,ac2,heating,feeding)
        _agent.set_state_AUTO_HEAT(True)
    except Exception as e:
        print("[E] init_agent error",e)

# 不存在 !! _thread.TIMEOUT_MAX
## print("_thread.TIMEOUT_MAX:",_thread.TIMEOUT_MAX)

def get_agent():
    with _agent_lock:
        if _agent is None:
            init_agent()
    return _agent




